package com.dubious.salesforce;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.dubious.salesforce.Node.InstallState;

public class ComponentInstaller {

    Map<String, Node> nodesByName = new HashMap<String, Node>();
    Set<String> installedNodes = new HashSet<String>();

    public void depend(String parent, List<String> children) throws Exception {
        Node parentNode = createOrGetNode(parent);
        for (String child : children) {
            Node childNode = createOrGetNode(child);
            if (!childNode.getChildren().contains(parent)) {
                // THE HACK:
                // childNode.addParent(parent);
                // parentNode.addChild(child);
                childNode.addChild(parent);
                parentNode.addParent(child);
            }
        }
    }

    public List<String> install(String name) throws Exception {
        return install(name, InstallState.EXPLICIT);
    }

    private List<String> install(String name, InstallState typeOfInstall) throws Exception {
        Node node = createOrGetNode(name);
        List<String> newInstalledNodes = new ArrayList<String>();

        if (typeOfInstall == InstallState.EXPLICIT
                && node.getInstallState() != InstallState.NOT_INSTALLED) {
            throw new Exception(name + " is already installed.");
        }

        if (node.getInstallState() == InstallState.NOT_INSTALLED) {
            // upgrade node from implict to explicit
            node.withInstallState(typeOfInstall);
            for (String parent : node.getParents()) {
                // any parents are installed IMPLICITLY and they are installed first
                newInstalledNodes.addAll(install(parent, InstallState.IMPLICIT));
            }
            newInstalledNodes.add(node.getName());
            installedNodes.add(node.getName());
        } else if (node.getInstallState() == InstallState.IMPLICIT) {
            // could be upgrading the state from IMPLICIT to EXPLICIT
            node.withInstallState(typeOfInstall);
        }

        return newInstalledNodes;
    }

    public List<String> remove(String nodeName) throws Exception {
        return remove(nodeName, InstallState.EXPLICIT);
    }

    private List<String> remove(String nodeName, InstallState typeOfRemove) throws Exception {
        Node node = nodesByName.get(nodeName);
        List<String> newRemovedNodes = new ArrayList<String>();

        if (typeOfRemove == InstallState.EXPLICIT
                && (node == null || node.getInstallState() == InstallState.NOT_INSTALLED)) {
            throw new Exception(nodeName + " is not installed.");
        }

        if (node != null && node.getInstallState() != InstallState.NOT_INSTALLED) {
            boolean canRemoveNode = true;
            if (typeOfRemove == InstallState.IMPLICIT
                    && node.getInstallState() == InstallState.EXPLICIT) {
                // cannot remove an ancestor that is EXPLICITLY installed
                canRemoveNode = false;
            }

            // check the children
            for (String child : node.getChildren()) {
                Node childNode = nodesByName.get(child);
                if (childNode.getInstallState() != InstallState.NOT_INSTALLED) {
                    // cannot remove this node
                    if (typeOfRemove == InstallState.EXPLICIT) {
                        // if this is the main node that was to be removed, it is actually an error
                        // case
                        throw new Exception(nodeName + " is still needed.");
                    }
                    canRemoveNode = false;
                    break;
                }
            }

            if (canRemoveNode) {
                node.withInstallState(InstallState.NOT_INSTALLED);
                newRemovedNodes.add(node.getName());
                installedNodes.remove(node.getName());
            }

            for (String parent : node.getParents()) {
                newRemovedNodes.addAll(remove(parent, InstallState.IMPLICIT));
            }
        }

        return newRemovedNodes;
    }

    public Set<String> list() {
        return Collections.unmodifiableSet(installedNodes);
    }

    private Node createOrGetNode(String nodeName) {
        Node node = nodesByName.get(nodeName);
        if (node == null) {
            node = new Node().withName(nodeName).withInstallState(InstallState.NOT_INSTALLED);
            nodesByName.put(nodeName, node);
        }

        return node;
    }

}
